import {HtmlNode} from "./HtmlNode.ts"
import {Stage} from "./Stage.ts"
import {NodeWrapper} from "./NodeWrapper.tsx"
import {applyBoundsToEl, boundsContainsPoint, combineBoundsArr, eachNode, isBoundsIntersect} from "./utils.ts"
import {proxyMouseMove} from "./gesture-helper.ts"
import {Bounds, CtrlElement, Point, Command, CommandType, DataSet, SelectNodeOptions} from "./interfaces.ts"

type ModifyType =
  'default'
  | 'move'
  | 'top'
  | 'bottom'
  | 'left'
  | 'right'
  | 'top-left'
  | 'top-right'
  | 'bottom-left'
  | 'bottom-right'

const CURSORS: Record<ModifyType, string> = {
  'default': 'cursor-default',
  'move': 'cursor-default',
  'top': 'cursor-ns-resize',
  'bottom': 'cursor-ns-resize',
  'left': 'cursor-ew-resize',
  'right': 'cursor-ew-resize',
  'top-left': 'cursor-nwse-resize',
  'top-right': 'cursor-nesw-resize',
  'bottom-left': 'cursor-nesw-resize',
  'bottom-right': 'cursor-nwse-resize',
}

const MODIFY_TYPE_PROPS: Record<ModifyType, string[]> = {
  'default': [],
  'move': ['x', 'y'],
  'left': ['x', 'w'],
  'top': ['y', 'h'],
  'right': ['w'],
  'bottom': ['h'],
  'top-left': ['x', 'y', 'w', 'h'],
  'top-right': ['y', 'w', 'h'],
  'bottom-left': ['x', 'w', 'h'],
  'bottom-right': ['w', 'h'],
}

export class CtrlLayer extends HtmlNode {
  protected _stage: Stage
  protected _selectBoxEl?: HTMLElement
  protected _boundsHoverEl?: HTMLElement
  protected _boundsBordersEl?: HTMLElement
  protected _boundsModifiersEl?: HTMLElement
  protected _hoverNode?: NodeWrapper
  protected _activeNode: NodeWrapper
  protected _selections: number[] = []
  protected _keydownSpace: boolean = false
  protected _draggingRoot: boolean = false
  protected _modifyingBounds: boolean = false
  protected _selectBoxActive: boolean = false
  protected _ctrlGroups = {} as Record<number, Bounds[]>
  protected _selectBounds = [] as Bounds[]
  private _lastMousedownTime = 0

  get selections(){
    return this._selections
  }

  protected get rootOffset() {
    const {offsetLeft, offsetTop} = this._stage.root.$el
    return {x: offsetLeft, y: offsetTop} as Point
  }

  protected get rootClientOffset() {
    const {left, top} = this._stage.root.$el.getBoundingClientRect()
    return {x: left, y: top} as Point
  }

  constructor(stage: Stage) {
    super({
      autoSetupEl: false
    })

    this.$setupEl()

    this._stage = stage
    this._activeNode = stage.root

    this._stage.host.classList.add('editable', 'cursor-default')

    document.addEventListener('keydown', this._onKeydownDocument)
    document.addEventListener('keyup', this._onKeyupDocument)
  }

  $historyCallback = (type: CommandType, action: string) => {
    //console.log(type, action)
    switch (type) {
      case CommandType.MODIFY_NODES:
        this._updateBoundsBordersAndModifiers(true)
        break
    }
  }

  $historyChangeCallback = (command: Command) => {
    this._stage.options.onHistoryChange?.(command)
  }

  protected _createEl() {
    const boundsHover = (el: HTMLElement) => {
      this._boundsHoverEl = el
    }
    const boundsBorders = (el: HTMLElement) => {
      this._boundsBordersEl = el
    }
    const boundsModifiers = (el: HTMLElement) => {
      this._boundsModifiersEl = el
    }
    const selectBox = (el: HTMLElement) => {
      this._selectBoxEl = el
    }

    return <div className="ctrl-layer"
                onMousemove={this._onMousemove}
                onMousedown={this._onMousedown}
                onMouseleave={this._onMouseleave}
    >
      <div ref={boundsBorders} className="bounds-borders"/>
      <div ref={boundsHover} className="bounds-hover"/>
      <div ref={boundsModifiers} className="bounds-modifiers"/>
      <div ref={selectBox} className="select-box"/>
    </div>
  }

  protected _onMousedown = (e: MouseEvent) => {
    e.stopPropagation()
    const {_lastMousedownTime} = this
    this._lastMousedownTime = Date.now()
    const asDblclick = this._lastMousedownTime - _lastMousedownTime < 300

    const {shiftKey} = e
    if (this._keydownSpace) {
      const rootEl = this._stage.root.$el
      const startPos = {x: rootEl.offsetLeft, y: rootEl.offsetTop}
      proxyMouseMove(e, {
        onMoveStart: () => {
          this._setDragging(true)
        },
        onMoveUpdate: (info) => {
          rootEl.style.left = (startPos.x + info.deltaX) + 'px'
          rootEl.style.top = (startPos.y + info.deltaY) + 'px'
        },
        onMoveEnd: () => {
          this._setDragging(false)
          this._updateHoverBounds()
          this._updateBoundsBordersAndModifiers()
        }
      })
    } else {
      let {_hoverNode: hoverNode} = this

      if (asDblclick && hoverNode && hoverNode.nodeData.nodeType === 'group') { //进入分组层
        this._activeNode = hoverNode
        this._onMousemove(e)
        hoverNode = this._hoverNode
        this.selectNode(hoverNode)
      }

      const {offsetLeft, offsetTop} = this._stage.host
      const hostOffset = {x: offsetLeft, y: offsetTop}
      const {x: rootX, y: rootY} = this.rootOffset
      const {_ctrlGroups} = this
      const mousedownPoint = {x: e.clientX - rootX - offsetLeft, y: e.clientY - rootY - offsetTop}
      let mousedownSelections = !!Object.entries(_ctrlGroups).find(([, bounds]) => boundsContainsPoint(combineBoundsArr(bounds), mousedownPoint))

      let oldSelections = this._selections.concat()
      let selections = this._selections.concat()
      let selectionsChanged = false
      proxyMouseMove(e, {
        onMoveUpdate: (info) => {
          if (info.firstMove) {
            if (hoverNode && (shiftKey || !mousedownSelections) && !this._selections.includes(hoverNode.id)) {
              this.selectNode(hoverNode, {
                multiMode: shiftKey,
                changeHistory: false,
              })
              mousedownSelections = true
            }
            if (mousedownSelections) {
              //拖动操作
              this._modifyBounds('move', e, oldSelections.concat())
              return true
            }

            this._activeNode = this._stage.root
            this.selectNode(undefined, {
              changeHistory: false,
            })
            oldSelections = []
            selections = []
            this.$el.classList.add('box-selecting')
          }

          //框选操作
          const {startX, startY, deltaX, deltaY} = info
          const active = Math.abs(deltaX) >= 1 || Math.abs(deltaY) >= 1
          const x = startX - hostOffset.x + (deltaX > 0 ? 0 : deltaX)
          const y = startY - hostOffset.y + (deltaY > 0 ? 0 : deltaY)
          const w = Math.abs(deltaX)
          const h = Math.abs(deltaY)
          applyBoundsToEl({x, y, w, h}, this._selectBoxEl!, 0)
          this._setSelectBoxActive(active)

          let changed = false
          const bounds = {x: x - rootX, y: y - rootY, w, h} as Bounds
          const {children} = this._activeNode
          for (let i = children.length - 1; i >= 0; i--) {
            const childNode = children[i]
            const index = selections.indexOf(childNode.id)
            if (shiftKey && index >= 0) {
              continue
            }
            if (isBoundsIntersect(bounds, childNode.globalBounds)) {
              if (index < 0) {
                selections.push(childNode.id)
                changed = true
              }
            } else {
              if (index >= 0) {
                selections.splice(index, 1)
                changed = true
              }
            }
          }
          if (changed) {
            this.$setSelections(selections, true, false)
            selectionsChanged = true
          }
        },
        onMoveEnd: (info) => {
          if (!info.moved) {
            this.selectNode(hoverNode, {
              multiMode: shiftKey,
            })
          }

          if (selectionsChanged) {
            this.$setSelections(oldSelections, false, false)
            this.$setSelections(selections, true)
          }
          this._setSelectBoxActive(false)
          this.$el.classList.remove('box-selecting')
        }
      })
    }
  }

  protected _onMousemove = (e: MouseEvent) => {
    if (this._keydownSpace || this._modifyingBounds) {
      return
    }

    if (e.target !== this.$el) {
      this._setHoverNode()
      return;
    }

    const {clientX, clientY} = e
    const {x: offsetX, y: offsetY} = this.rootClientOffset
    const mousePoint = {x: clientX - offsetX, y: clientY - offsetY}

    let hoverNode: NodeWrapper | undefined

    let layer: NodeWrapper | undefined = this._activeNode
    while (layer) {
      const children = layer.children
      for (let i = children.length - 1; i >= 0; i--) {
        const childNode = children[i]
        if (childNode !== this._activeNode && boundsContainsPoint(childNode.globalBounds, mousePoint)) {
          hoverNode = childNode
          break
        }
      }
      if (hoverNode) {
        break
      }
      layer = layer.parent
    }

    this._setHoverNode(hoverNode)
  }

  protected _onMouseleave = (e: MouseEvent) => {
    this._setHoverNode(undefined)
  }

  protected _setHoverNode(hitChildNode?: NodeWrapper) {
    if (this._hoverNode !== hitChildNode) {
      this._hoverNode = hitChildNode
      this._setHoverBoundsBorders(hitChildNode)
      const {onHoverNode} = this._stage.options
      onHoverNode && onHoverNode(hitChildNode)
    }
  }

  protected _getSelectionNodes(excludeLocked = true) {
    const nodes = [] as NodeWrapper[]
    const {_selections} = this
    eachNode(this._stage.root, childNode => {
      if (_selections.includes(childNode.id)) {
        nodes.push(childNode)
      }
    })
    return nodes
  }

  protected _dealCtrl = (e: MouseEvent) => {
    e.stopPropagation()
    const ctrlEl = e.target as CtrlElement
    const ctrlType = ctrlEl['data-type'] as ModifyType

    this._modifyBounds(ctrlType, e)
  }

  protected _modifyBounds(type: ModifyType, e: MouseEvent, oldSelections: number[] = []) {
    const selectionNodes = this._getSelectionNodes()
    selectionNodes.forEach((node: NodeWrapper) => {
      node.$shortcutBounds = Object.assign({}, node.bounds!)
    })
    //console.log(type, selectionNodes)

    proxyMouseMove(e, {
      onMoveStart: (info) => {
        this._modifyingBounds = true
        this.$el.classList.add(CURSORS[type])
        this.$el.classList.add('modifying-bounds')
        //console.log(info)
      },
      onMoveUpdate: (info) => {
        //console.log(info)
        selectionNodes.forEach((node: NodeWrapper) => {
          const {x: sx, y: sy, w: sw, h: sh} = node.$shortcutBounds
          const bounds = node.bounds!
          const {deltaX, deltaY, moved} = info
          switch (type) {
            case 'move':
              bounds.x = sx + deltaX
              bounds.y = sy + deltaY
              break
            case 'left':
              bounds.x = sx + deltaX
              bounds.w = sw - deltaX
              break
            case 'top':
              bounds.y = sy + deltaY
              bounds.h = sh - deltaY
              break
            case 'right':
              bounds.w = sw + deltaX
              break
            case 'bottom':
              bounds.h = sh + deltaY
              break
            case 'top-left':
              bounds.x = sx + deltaX
              bounds.w = sw - deltaX
              bounds.y = sy + deltaY
              bounds.h = sh - deltaY
              break
            case 'top-right':
              bounds.w = sw + deltaX
              bounds.y = sy + deltaY
              bounds.h = sh - deltaY
              break
            case 'bottom-left':
              bounds.x = sx + deltaX
              bounds.w = sw - deltaX
              bounds.h = sh + deltaY
              break
            case 'bottom-right':
              bounds.w = sw + deltaX
              bounds.h = sh + deltaY
              break
          }
          node.$applyBounds()
          node.$updateGlobalBounds()
        })
        this._updateBoundsBordersAndModifiers(true)
      },
      onMoveEnd: (info) => {
        this._modifyingBounds = false
        this.$el.classList.remove('modifying-bounds')

        if (info.moved) {
          this.$el.classList.remove(CURSORS[type])
          let modifyProps = MODIFY_TYPE_PROPS[type]
          this._stage.commands.modifyNodes(selectionNodes.map(node => ({
            node,
            data: modifyProps.reduce((pv, cv) => {
              const bounds = node.nodeData!.bounds!
              pv['bounds.' + cv] = bounds[cv as keyof typeof bounds]
              return pv
            }, {} as DataSet),
            oldData: {bounds: node.$shortcutBounds},
          })), oldSelections ?? this._selections.concat())

          selectionNodes.forEach(node => {
            node.$updateGlobalBounds(true)
          })
        }
      },
    })
  }

  protected _setHoverBoundsBorders(node?: NodeWrapper) {
    const {_boundsHoverEl, _selections} = this
    if (_boundsHoverEl && _boundsHoverEl.style) {
      const boundsBordersElStyle = _boundsHoverEl.style
      if (node && !_selections.includes(node.id)) {
        boundsBordersElStyle.visibility = 'visible'
        this._updateHoverBounds()
      } else {
        boundsBordersElStyle.visibility = 'hidden'
      }
    }
  }

  protected _updateHoverBounds() {
    const {_boundsHoverEl, _hoverNode} = this
    if (_hoverNode && _boundsHoverEl) {
      const {x = 0, y = 0, w = 0, h = 0} = _hoverNode.globalBounds
      applyBoundsToEl({x, y, w, h}, _boundsHoverEl, 2, this.rootOffset)
    }
  }

  protected _updateBoundsBordersAndModifiers(boundsOnly = false) {
    const {rootOffset, _ctrlGroups, _selectBounds} = this

    const boundsModifiers = this._boundsModifiersEl!.children
    const groupArr = Object.values(_ctrlGroups)
    for (let i = 0; i < groupArr.length; i++) {
      const group = groupArr[i]
      let el = boundsModifiers.item(i) as CtrlElement

      if (!boundsOnly) {
        if (!el) {
          el = this._createBoundsModifierEl()
          this._boundsModifiersEl!.appendChild(el)
        }
        el.style.visibility = 'visible'
      }
      const bounds = combineBoundsArr(group)
      applyBoundsToEl(bounds, el, 1, rootOffset)
    }
    if (!boundsOnly) {
      for (let i = groupArr.length; i < boundsModifiers.length; i++) {
        let el = boundsModifiers.item(i) as HTMLElement
        el.style.visibility = 'hidden'
      }
    }

    const boundsBorders = this._boundsBordersEl!.children
    for (let i = 0; i < _selectBounds.length; i++) {
      const selectBound = _selectBounds[i]
      let el = boundsBorders.item(i) as HTMLElement

      if (!boundsOnly) {
        if (!el) {
          el = this._createBoundsBorderEl()
          this._boundsBordersEl!.appendChild(el)
        }
        el.style.visibility = 'visible'
      }
      applyBoundsToEl(selectBound, el, 1, rootOffset)
    }
    if (!boundsOnly) {
      for (let i = _selectBounds.length; i < boundsBorders.length; i++) {
        let el = boundsBorders.item(i) as HTMLElement
        el.style.visibility = 'hidden'
      }
    }
  }

  protected _setSelectBoxActive(active: boolean) {
    if (this._selectBoxActive !== active) {
      this._selectBoxActive = active
      if (active) {
        this._selectBoxEl!.classList.add('active')
      } else {
        this._selectBoxEl!.classList.remove('active')
      }
    }
  }

  protected _updateSelection() {
    const {_selections} = this
    //console.log(_selections)

    const ctrlGroups = this._ctrlGroups = {} as Record<number, Bounds[]>
    const selectBounds = this._selectBounds = [] as Bounds[]

    eachNode(this._stage.root, (childNode, index, parentNode) => {
      const {id: childId} = childNode
      if (_selections.includes(childId)) {
        selectBounds.push(childNode.globalBounds)
        if (parentNode) {
          const {id: parentId} = parentNode
          let group = ctrlGroups[parentNode.id]
          if (!group) {
            group = ctrlGroups[parentId] = []
          }
          group.push(childNode.globalBounds)
        }
      }
    })

    //console.log('ctrlGroups:', ctrlGroups)

    this._updateBoundsBordersAndModifiers()
    this._setHoverBoundsBorders(this._hoverNode)
  }

  protected _createBoundsModifierEl(): CtrlElement {
    return <div className="bounds-modifier" onMousedown={this._dealCtrl}>
      <div className="ctrl-handler side top cursor-ns-resize" data-type="top"/>
      <div className="ctrl-handler side bottom cursor-ns-resize" data-type="bottom"/>
      <div className="ctrl-handler side left cursor-ew-resize" data-type="left"/>
      <div className="ctrl-handler side right cursor-ew-resize" data-type="right"/>
      <div className="ctrl-handler corner top-left cursor-nwse-resize" data-type="top-left"/>
      <div className="ctrl-handler corner top-right cursor-nesw-resize" data-type="top-right"/>
      <div className="ctrl-handler corner bottom-left cursor-nesw-resize" data-type="bottom-left"/>
      <div className="ctrl-handler corner bottom-right cursor-nwse-resize" data-type="bottom-right"/>
    </div>
  }

  protected _createBoundsBorderEl(): HTMLElement {
    return <div className="bounds-border"/>
  }

  protected _setKeydownSpace(keydown: boolean) {
    if (this._keydownSpace != keydown) {
      this._keydownSpace = keydown
      if (keydown) {
        this._stage.host.classList.add('drag-mode')
      } else if (!this._draggingRoot) {
        this._stage.host.classList.remove('drag-mode')
      }
    }
  }

  protected _setDragging(dragging: boolean) {
    if (this._draggingRoot != dragging) {
      this._draggingRoot = dragging
      if (dragging) {
        this._stage.host.classList.add('dragging')
      } else {
        this._stage.host.classList.remove('dragging')
        if (!this._keydownSpace) {
          this._keydownSpace = true
          this._setKeydownSpace(false)
        }
      }
    }
  }

  protected _onKeydownDocument = (e: KeyboardEvent) => {
    switch (e.key) {
      case ' ':
        this._setKeydownSpace(true)
        break
    }
  }

  protected _onKeyupDocument = (e: KeyboardEvent) => {
    switch (e.key) {
      case ' ':
        this._setKeydownSpace(false)
        break
    }
  }

  selectNode = (node?: NodeWrapper, options: SelectNodeOptions = {}) => {
    const {
      multiMode = false,
      toggleMode = true,
      unselect = false,
      updateSelection = true,
      changeHistory = true,
    } = options
    //console.log('click', node)
    const nodeId = node?.id
    const selections = this._selections.concat()
    const length = selections.length
    let changed = false
    const index = nodeId ? selections.indexOf(nodeId) : -1
    const included = index >= 0
    if (multiMode) {  //多选模式
      if (nodeId) {
        if (toggleMode) { //切换模式
          if (included) {
            selections.splice(index, 1)
          } else {
            selections.push(nodeId)
          }
          changed = true
        } else if (unselect) {
          if (included) {
            selections.splice(index, 1)
            changed = true
          }
        } else {  //追加模式
          if (!included) {
            selections.push(nodeId)
            changed = true
          }
        }
      }
    } else {  //单选模式
      if (nodeId) {
        if (!included || length > 1) {
          selections.splice(0)
          selections.push(nodeId)
          changed = true
          if (node?.parent) {
            this._activeNode = node.parent
          }
        }
      } else {
        if (length > 0) {
          selections.splice(0)
          changed = true
          this._activeNode = this._stage.root
        }
      }
    }
    if (updateSelection) {
      this.$setSelections(selections, updateSelection, changeHistory)
    }
    return changed
  }

  $setSelections(selections: number[], update = true, changeHistory = true) {
    const {_selections} = this
    const oldSelections = _selections.concat()
    _selections.splice(0)
    _selections.push(...selections)
    const changed = _selections.sort().join(',') != oldSelections.sort().join(',')

    if (update) {
      this._updateSelection()
    }
    if (changed && changeHistory) {
      //console.log('setSelections:', selections)
      this._stage.commands.setSelections(selections, oldSelections)
    }
    if (changed && update) {
      const {onSelectionChange} = this._stage.options
      onSelectionChange && onSelectionChange(_selections)
    }
  }

  $updateGlobalBounds() {
    this._stage.root.$updateGlobalBounds(true)
  }

  setHoverNode(id: number) {
    let node: NodeWrapper | undefined
    if (id) {
      eachNode(this._stage.root, childNode => {
        if (childNode.id === id) {
          node = childNode
          return true
        }
      })
    }
    this._setHoverNode(node)
  }

  destroy() {
    document.removeEventListener('keydown', this._onKeydownDocument)
    document.removeEventListener('keyup', this._onKeyupDocument)
  }
}